#include <croco/ip/tcp.hpp>
#include <croco/common_exceptions.hpp>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cassert>
#include <sstream>

namespace cip = croco::ip;

namespace {

class socket_address
{
public:
    socket_address(const char* address, int port)
    {
        assert(address);
        sockaddr_in* sa = reinterpret_cast<sockaddr_in*>(&m_storage);
        memset(sa, 0, sizeof(sockaddr_in));
        sa->sin_family = AF_INET;
        sa->sin_port = htons(port);
        sa->sin_addr.s_addr = inet_addr(address);
        m_size = sizeof(sockaddr_in);
    }
    const sockaddr* raw_addr() const { return reinterpret_cast<const sockaddr*>(&m_storage); }
    int raw_addr_size() const { return m_size; }

private:
    sockaddr_storage m_storage;
    int m_size;
};

} // anonymous namespace

cip::tcp::socket_move::socket_move(cip::connected_tcp& s) : handle(-1)
{
    std::swap(handle, s.m_handle);
}

cip::tcp::tcp()
: m_handle(socket(AF_INET, SOCK_STREAM, 0))
{
}

cip::tcp::~tcp()
{
    try
    {
        close();
    }
    catch(...) {}
}

void cip::tcp::bind(const char* address, int port)
{
    socket_address sa(address, port);
    int optval = 1;
    int result = ::setsockopt(m_handle, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const void*>(&optval), sizeof(optval));
    if(result == -1)
    {
        throw os_error() << boost::errinfo_api_function("setsockopt") << boost::errinfo_errno(errno);
    }
    result = ::bind(m_handle, sa.raw_addr(), sa.raw_addr_size());
    if(result == -1)
    {
        std::stringstream ss;
        ss << address << ":" << port;
        throw os_error() << boost::errinfo_api_function("bind") << boost::errinfo_errno(errno) << more_info(ss.str());
    }
}

void cip::tcp::connect(const char* address, int port)
{
    socket_address sa(address, port);
    int result = ::connect(m_handle, sa.raw_addr(), sa.raw_addr_size());
    if(result == -1)
    {
        throw os_error() << boost::errinfo_api_function("connect") << boost::errinfo_errno(errno);
    }
}

void cip::tcp::listen()
{
    int result = ::listen(m_handle, SOMAXCONN);
    if(result == -1)
    {
        throw os_error() << boost::errinfo_api_function("listen") << boost::errinfo_errno(errno);
    }
}

cip::connected_tcp cip::tcp::accept()
{
    native_handle_type handle = ::accept(m_handle, 0, 0);
    if(handle == -1)
    {
        throw os_error() << boost::errinfo_api_function("accept") << boost::errinfo_errno(errno);
    }
    return connected_tcp(handle);
}

int cip::tcp::receive(char* buffer, std::size_t size)
{
    int result = ::read(m_handle, buffer, size);
    if(result < 0)
    {
        throw os_error() << boost::errinfo_api_function("read") << boost::errinfo_errno(errno);
    }
    return result;
}

int cip::tcp::send(const char* buffer, std::size_t size)
{
    int result = ::write(m_handle, buffer, size);
    if(result < 0)
    {
        throw os_error() << boost::errinfo_api_function("write") << boost::errinfo_errno(errno);
    }
    return result;
}

void cip::tcp::close()
{
    if(m_handle == -1)
    {
        return;
    }
    if(::shutdown(m_handle, SHUT_RDWR) < 0)
    {
        if(errno != ENOTCONN && errno != EBADF)
        {
            throw os_error() << boost::errinfo_api_function("shutdown") << boost::errinfo_errno(errno);
        }
    }
    if(::close(m_handle) < 0)
    {
        if(errno != EBADF)
        {
            throw os_error() << boost::errinfo_api_function("close") << boost::errinfo_errno(errno);
        }
    }
}

cip::connected_tcp& cip::connected_tcp::operator=(socket_move sm)
{
    set_native_handle(sm.handle);
    return *this;
}

cip::connected_tcp::operator socket_move()
{
    socket_move result(*this);
    return result;
}
